import _ from "lodash";
import fg from 'fast-glob';
import fs from 'fs-extra';
import path from "path";
import { findFiles, md5, readVersionTxt, replaceInFileOrThrow } from "../../tools/vendor.js";
import { BaseTask, TaskResult } from "./BastTask.js";
import { toolchain } from "../../toolchain.js";
import { ChannelCfg, Nullable } from "../../typings";
import { CommonEnv } from "../CommonEnv.js";
import { EFileErrorType, alertErrorFile, sendBuildFailureAlert } from "../../tools/alert.js";
import { Cmd } from "../../tools/Cmd.js";
import { SVNHelper } from "../../tools/SVNHelper.js";
import { EErrorType } from "../../LogParser.js";
import { SVNSumary, SVNOperationTask } from "./SVNOperationTask.js";
import { WebGLEnv } from "../../webgl/WebGLEnv.js";
import { SetImportFormatTask } from "./SetImportFormatTask.js";

export class BuildAssetsTask extends BaseTask {
    async run(): Promise<TaskResult<void>> {    
        // 如果是马甲包，则需要先把资源copy到主assets上
        console.time('copy资源到主assets');
        await this._prepare_majiabao_res();
        console.timeEnd('copy资源到主assets');
    
        console.time('构建jsonData');
        await this._buildJsonData();
        console.timeEnd('构建jsonData');

        console.time('构建AB包');
        await this._createAssetBundle();
        console.timeEnd('构建AB包');

        console.time('构建非ab包资源');
        await this._buildRawRes();
        console.timeEnd('构建非ab包资源');

        await this._backBuildinTxtFile();

        await this.copyTsscriptMaps();

        await this.upLoadSvnVerFile();

        // 拷贝其他资源
        const copytasks = toolchain.params.channelCfg.copytasks;
        if (copytasks != null) {
            for (const task of copytasks) {
                const src = await fg(task.src, { cwd: toolchain.params.workSpacePath });
                for (const srcFile of src) {
                    await fs.copyFile(path.join(toolchain.params.workSpacePath, srcFile), path.join(toolchain.params.workSpacePath, task.dst));
                }
            }
        }
        
        return { success: true, errorCode: 0 };
    }
    
    private async _prepare_majiabao_res() {
        // 删除主项目的StreamingAssets
        const streamingAssetsPath = path.join(toolchain.params.workSpacePath, 'Assets/StreamingAssets');
        await fs.remove(streamingAssetsPath);
        //   await fs.rm(streamingAssetsPath, {force: true, recursive: true});
            
        console.log ("@@@ start build majiabao");

        // todo 下面这两个svn步骤有啥必要？？
        // 因为已经没有构马甲包了，下述删除update操作拖慢构建速度，去掉
    
        // console.log('@@ remove main\\Assets\\Plugins non versioned by svn');
        // const plugins = path.join(toolchain.params.workSpacePath, 'Assets/Plugins');
        // fs.removeSync(plugins);
        // await toolchain.svnClient.update(plugins);
        
        // console.log('@@ remove main\\Assets\\Scripts non versioned by svn');
        // const scripts = path.join(toolchain.params.workSpacePath, 'Assets/Scripts');
        // fs.removeSync(scripts);
        // await toolchain.svnClient.update(scripts);
    
        // 初次Copy，将资源打进AB包中
        await this._copyChannelAssets();
    }
    
    private async _copyChannelAssets() {
        const channelCfg = toolchain.params.channelCfg;
        if(channelCfg.common) {
            this._copyChannelAssetsToMain(channelCfg, path.join(toolchain.params.workSpacePath, toolchain.params.platformPath, channelCfg.common, 'project'))
            this._copyChannelAssetsToMain(channelCfg, path.join(toolchain.params.workSpacePath, toolchain.params.platformPath, channelCfg.path, 'project'))
        }            
    }
    
    private async _copyChannelAssetsToMain(c: ChannelCfg, channelPath: string) {
        console.log(`@@ copy channel\\Assets to main\\Assets, frompath: ${channelPath}`);
        const channelSrc = path.join(channelPath, 'Assets');
        const mainDst = path.join(toolchain.params.workSpacePath, 'Assets');
        if(fs.existsSync(channelSrc)) {
            await fs.copy(channelSrc, mainDst);
        }
        if(this.cmdOption.platform == 'Android') {
            await this._modifyManifestFile(c, mainDst);
        }
    }
      
    private async _modifyManifestFile(c: ChannelCfg, mainDst: string) {    
        const bundleId = c.bundleId;
        const shortName = c.shortName || '';
        const qqAppid = _.trim(c.qqAppid || '');
        const wxAppid = c.wxAppid || '';
        const appid = c.appid || '';
        const targetSdkVersion = c.targetSdkVersion || '21';
        const androidPermission = c.androidPermission || '';
        console.log(`@@@@ qqappid:[${qqAppid}]`);

        const manifest = path.join(mainDst, 'Plugins/Android/AndroidManifest.xml');
        console.log('@@ manifest:' + manifest);
        if (!fs.existsSync(manifest)) {
            console.log('manifest not exists');
            return;
        }
        let s = await fs.readFile(manifest, 'utf-8');

        s = s.replace('${PACKAGE_NAME}', bundleId).replace('${SHORT_NAME}', shortName)
        .replace('${QQAPPID}', qqAppid).replace('${WXAPPID}', wxAppid).replace('${ANDROID_PERMISSION}', androidPermission)
        .replace('${TARGET_SDK_VERSION}', targetSdkVersion)
        .replace('${APP_ID}', appid)
        .replace(/(?<=android:targetSdkVersion)\s* = \s*"\s*\d+\s*"/, ` = "${targetSdkVersion}"`);
        
        // 新的替换规则, 查找 ${varname}
        s.replace(/\$\{(\w+)\}/mg, (substring: string, ...args: any[]) => {
            let key = args[0];
            if(key in c) {
                return (<any>c)[key];
            }
            return substring;
        });
        
        await fs.writeFile(manifest, s, 'utf-8');
    }

    private async _buildJsonData() {
        if(toolchain.params.channelCfg.build_bjson !== false && this.cmdOption.platform == 'Android') {
            const exe = path.join(this.cmdOption.oldToolsRoot, 'FYMGBuild/lib/bjsonpack.exe');
            await toolchain.jsonBuilder.build_bjson(exe, toolchain.params, toolchain.svnClient, toolchain.logParser);
        }
    }

    private async _createAssetBundle() {
        console.log('@@ create asset bundle');
        fs.ensureDirSync(toolchain.params.uploadPath);
    
        let uploadPath = '';
        if (toolchain.option.webglRuntime != 'minigame') {
            // 构建小游戏的话，由于需要压缩贴图，ab包打出来后先不上传到res server
            uploadPath = toolchain.params.uploadPath;
            if(uploadPath[uploadPath.length - 1] != path.sep) uploadPath += path.sep;
        }
        const params = ['-batchmode', '-projectPath', toolchain.params.workSpacePath, '-executeMethod', 'BuildCommand.CreateAssetBundles', 
        '-platform', this.cmdOption.platform, '-private_obb', String(toolchain.params.BuildPrivateOBB || false), 
        '-defines', 'PUBLISH', '-gameid', String(toolchain.params.channelCfg.gameid), '-quit'];
        if (uploadPath) {
            params.push('-UploadResDir', uploadPath);
        }
        if (toolchain.option.isTiShen) {
            params.push('-IsTiShen', String(toolchain.option.isTiShen));
        }
        
        const cmd = await toolchain.unity.runUnityCommand(this.cmdOption, params, 'CreateAB', true);
        console.log('@@ start get unrecognizedAssets');
        const unrecognizedAssets = toolchain.logParser.getUnrecognizedAssets(cmd.output);
        console.log(`@@ get unrecognizedAssets ok, count: ${unrecognizedAssets.length}`);
        if(unrecognizedAssets.length > 0) {
            // 取消正在构建的任务，有时会把正在构建的asset文件在unity的Library的meta的缓冲文件破坏掉
            // 导致以后构建出现 Unrecognized assets cannot be included in AssetBundles: "xxx" 
            // 需要将该文件在unity中对应的缓冲文件删除后重新构建
            console.log('@@ start delete unity libray metas');
            let rt = this._deleteUnityLibraryMetas(unrecognizedAssets);
            console.log('@@ delete unity libray metas ok');
            if(!rt) {
                await this.checkCmdOutput(cmd, toolchain.unity.lastErrCode);
            }
            
            console.log('@@ start create assetbundle again');
            await toolchain.unity.runUnityCommand(this.cmdOption, params, 'CreateAB', false);
            console.log('@@ create assetbundle again ok');

        } else {
            await this.checkCmdOutput(cmd, toolchain.unity.lastErrCode);
        }

        let resVersion = await toolchain.unity.getResVersion();
        if (toolchain.option.platform == 'WebGL' && toolchain.option.webglRuntime == 'minigame') {
            // 小游戏每次都会递增版本号，原因是:
            // 由于微信需要压缩贴图，而压缩贴图需要在导出微信工程后进行，导出微信工程又需要提前知道资源版本号，故指定死规则：微信项目每次构建均递增资源版本号
            resVersion++;
        }
        console.log('本次构建资源版本号：', resVersion);
        toolchain.buildInfo.resVersion = resVersion;

        // 修改ResLoader.cs中的assetbundleVersion
        if (toolchain.option.platform == 'WebGL') {
            const resLoaderFile = path.join(toolchain.params.workSpacePath, CommonEnv.RESLOADER_CS);
            await replaceInFileOrThrow(resLoaderFile, 'assetbundleVersion = 12345', `assetbundleVersion = ${resVersion}`);
        }
    }

    private async _buildRawRes() {
        if (this.cmdOption.platform != "WebGL") { // 目前只支持webgl
            return;
        }

        let assetsPath = "";
        if (this.cmdOption.platform == "WebGL")
            assetsPath = "assets/webgl";
        else if (this.cmdOption.platform == "Android")
            assetsPath = "assets/android";
        else if (this.cmdOption.platform == "iOS") 
            assetsPath = "assets/ios";
        else if (this.cmdOption.platform == "Windows") 
            assetsPath = "assets/windows";
        else {
            console.log("failed:");
            return;
        }

        fs.ensureDirSync(path.join(toolchain.params.uploadPath, assetsPath));
        
        const resversionfile = path.join(toolchain.params.uploadPath, assetsPath, "version.txt");
        if (!fs.existsSync(resversionfile)) {
            console.log("failed:");
            return;
        }

        let rawres:{[path:string]:string} = {};
        const [resver] = await readVersionTxt(resversionfile);
        const assetSourcesPath = path.join(toolchain.params.workSpacePath, "Assets/AssetSources");
        const bundleJsonPath = path.join(assetSourcesPath, "data/bundle.json");
        if (!fs.existsSync(bundleJsonPath)) {
            console.log("not find: " + bundleJsonPath);
            return;
        }

        //--- loop
        const srcPath = bundleJsonPath;
        const fileMd5 = md5(await fs.readFile(srcPath)).substring(0, WebGLEnv.MD5Len);
        const extName = path.extname(srcPath);
        const refPath = srcPath.substring(assetSourcesPath.length + 1).replace(/\\/g, "/");
        const dstPath = path.join(toolchain.params.uploadPath, assetsPath, refPath.replace(extName, "_" + fileMd5 + extName));
        await fs.copyFile(srcPath, dstPath);

        rawres[refPath] = fileMd5;
        //--- end loop

        await fs.writeFile(path.join(toolchain.params.uploadPath, assetsPath, "rawres_" + resver + ".txt"), JSON.stringify(rawres));
    }

    private async checkCmdOutput(cmd: Cmd, errcode: number): Promise<void> {
        toolchain.logParser.parseUnityBuildLog(cmd.output, errcode);
        if(toolchain.logParser.error) {
            let suggestRetry = false;
            if (toolchain.logParser.isKindOf(EErrorType.CompilationFailed)) {
                const errorFiles = toolchain.logParser.errorFiles;
                // 2022.3.14f1新增c#会报CS0103，过滤掉
                const ignoreCSE = ['CS0103', 'CS0234', 'CS0246'];
                if (toolchain.params.unityVer == '2022.3.14f1' && errorFiles.length > 0 && errorFiles.every(v => ignoreCSE.includes(v.errorCode))) {
                    const svnRst = toolchain.taskHelper.getResult<SVNSumary>(SVNOperationTask.name);
                    if (svnRst != null && svnRst.data?.addNewCsharps?.length) {
                        suggestRetry = true;
                    }
                }
                if (!suggestRetry) {
                    await alertErrorFile(errorFiles[0].file, EFileErrorType.CodeError, toolchain.logParser.error);
                }
            }
            await sendBuildFailureAlert(toolchain.logParser.error, true, suggestRetry);
            process.exit(1);
        }
    }

    private _deleteUnityLibraryMetas(unrecognizedAssets: string[]) {
        const guidpatt = /guid: ([0-9a-fA-F]{32})/;
        for(let uasset of unrecognizedAssets) {
            uasset = uasset.replace('Assets/AssetSources/', '');
            const metaPath = path.join(toolchain.params.workSpacePath, 'Assets/AssetSources', uasset + '.meta');
            try {
                const s = fs.readFileSync(metaPath, 'utf-8');
                const mrt = s.match(guidpatt);
                const guid = mrt![0];
                const metaFile = path.join(toolchain.params.workSpacePath, 'Library/metadata/' + guid.substring(0, 2) + '/' + guid);
                fs.removeSync(metaFile);
                fs.removeSync(metaFile + '.info');
                return true;
            } catch(e) {
                console.error(e);
                return true;
            }
        }
        return false;
    }

    private async _backBuildinTxtFile() {
        const src = path.join(toolchain.params.workSpacePath, 'publish/buildinAB.txt');
        if (fs.existsSync(src)) {
            const back = path.join(toolchain.params.workSpacePath, 'publish/buildinAB_bak.txt');
            await fs.copyFile(src, back);
        }
    }
    
    /**
     * copy ts的map到资源服务器上
     **/
    private async copyTsscriptMaps(): Promise<void> {
        // webgl目前不适用sourcemap
        if (this.cmdOption.platform == 'WebGL') return;
        console.log('@@ start copy tsscript sourcemap');
        const dstmaps = path.join(toolchain.params.uploadPath, toolchain.unity.getAssetsDir(), 'tsscriptmaps');
        fs.ensureDir(dstmaps);
        const srcmaps = path.join(toolchain.params.workSpacePath, CommonEnv.TsOutput);
        
        if(this.cmdOption.platform != 'Windows') {
            // 保留老的copy方式，防止有的项目没同步代码
            fs.copySync(srcmaps, dstmaps, {filter: (src: string, dest: string)=>{return path.extname(src) == '.map'}});
        }
    
        const md5map: {[key: string]: string} = {};
        const resversionfile = path.join(toolchain.params.uploadPath, toolchain.unity.getAssetsDir(), 'version.txt');
        const [resver] = await readVersionTxt(resversionfile);
        console.log('@@ resver:' + resver);
        
        console.log('@@ start copy tsscript sourcemap by version');
        const files = await findFiles(srcmaps, ['.map'], '+');
        for(let file of files) {
            let fileRelativePath = path.relative(srcmaps, file).replace(/\\/g, '/');
            let fileNameFormated = fileRelativePath.substring(0, fileRelativePath.length - 7);
            md5map[fileNameFormated] = md5(fs.readFileSync(file, 'utf-8'));
            const dstfile = path.join(dstmaps, fileNameFormated + '.' + md5map[fileNameFormated] + '.js.map');
            fs.ensureDirSync(path.dirname(dstfile));
            fs.copyFileSync(file, dstfile);
        }
        const resverFile = path.join(dstmaps, 'ver.' + resver + '.txt');
        fs.ensureDirSync(path.dirname(resverFile));
        fs.writeFileSync(resverFile, JSON.stringify(md5map));
        console.log('@@ copy tsscript sourcemap by version ok');
    }

    // 生成当前版本的svnver.txt/xlssvnver.txt到uploadRes文件夹
    private async upLoadSvnVerFile() {
        let uploadPath = path.join(toolchain.params.uploadPath, toolchain.unity.getAssetsDir());
        // 小游戏构建此时还没上传ab包到http服务器，需等WebGLCompressTextureTask，故首次构建还没创出这个目录
        await fs.ensureDir(uploadPath);
        
        let codeUrl = toolchain.params.svn.urlMap.code[toolchain.params.projectType];
        let codeVer = await SVNHelper.getVersion(codeUrl);
        console.log('@@ code svnver:' + codeVer);
        fs.writeFileSync(path.join(uploadPath, 'svnver.txt'), codeVer);

        let xlsUrl = toolchain.params.svn.urlMap.xls[toolchain.params.projectType];
        let xlsVer = await SVNHelper.getVersion(xlsUrl);
        console.log('@@ xls svnver:' + xlsVer);
        fs.writeFileSync(path.join(uploadPath, 'xlssvnver.txt'), xlsVer);
    }

    /**依赖前置任务，被依赖的任务无视skip命令 */
    get dependencies(): Nullable<string[]> {
        return [SetImportFormatTask.name];
    }
}
